package com.atguigu.gmall.gateway.filter;

import com.atguigu.gmall.common.constant.RedisConst;
import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.common.util.IpUtil;
import com.atguigu.gmall.common.util.JSONs;
import com.atguigu.gmall.gateway.properties.UserAuthUrlProperties;
import com.atguigu.gmall.user.entity.UserInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.MalformedURLException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * @author lfy
 * @Description 进行统一的用户校验、id透传
 * @create 2023-07-28 14:30
 * <p>
 * 响应式编程
 * 命令式编程
 */
@Slf4j
@Component
public class UserAuthGlobalFilter implements GlobalFilter {

    @Autowired
    UserAuthUrlProperties urlProperties;

    AntPathMatcher matcher = new AntPathMatcher();

    @Autowired
    StringRedisTemplate redisTemplate;


    /**
     * @param exchange the current server exchange: 请求响应就是 exchange
     * @param chain    provides a way to delegate to the next filter： chain代表filter链
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange,
                             GatewayFilterChain chain) {

        String requestPath = exchange.getRequest().getPath().value();

        //用户id透传处理
        log.info("请求进来；路径：{}", requestPath);


        //1、所有静态资源请求直接放行  //.css .js .png  .mp3
        long count = urlProperties.getAnyone()
                .stream()
                .filter(item -> matcher.match(item, requestPath))
                .count();
        if (count > 0) {
            //满足静态资源规则，直接放行
            log.info("静态资源-放行：{}", requestPath);
            return chain.filter(exchange);
        }

        //2、所有微服务内部调用，禁止浏览器直接访问
        long denyCount = urlProperties.getDenyUrls()
                .stream()
                .filter(item -> matcher.match(item, requestPath))
                .count();
        if (denyCount > 0) {
            log.warn("内部路径，拒绝浏览器访问：【{}】", requestPath);
            Result<String> result = Result.build("", ResultCodeEnum.PERMISSION);
            Mono<Void> mono = responseJSON(exchange, result);
            //写出数据
            return mono;
        }

        //3、如果必须登录才能访问，登录验证
        long mustLoginCount = urlProperties.getMustLogin().stream()
                .filter(item -> matcher.match(item, requestPath))
                .count();
        if (mustLoginCount > 0) {
            //3.1、验证是否登录了？
            String token = getCookieOrHeaderValue(exchange,RedisConst.USER_TOKEN_HEADER);
            //3.2、验证这个token是否是对的
            UserInfo info = getLoginUserInfo(token);
            if (info == null) {
                //redis并没有此token对应的用户，打回到登录页
                return redirectLogin(exchange);
            }else {
                return userIdThrought(exchange, chain, info);
            }
        }


        //能走到这里的有一种请求：【普通请求】
        //如果携带了token就检查登录，进行userid透传。如果没带就拉倒，直接放行。
        //4、验证是否登录了？
        UserInfo info = null;
        String token = getCookieOrHeaderValue(exchange,RedisConst.USER_TOKEN_HEADER);
        if(StringUtils.hasText(token)){
            //5、验证这个token是否是对的
            info = getLoginUserInfo(token);
            if(info == null){
                //5.1、token在redis没有保存。这基本是一个假token
                return redirectLogin(exchange);
            }else {
                //IP防伪
                //有可能token被盗用，需要进行ip判断
                String ipAddress = info.getIpAddress();
                //这次请求ip和redis当时绑定的设备ip是否一致
                ServerHttpRequest request = exchange.getRequest();
                //获取当前请求ip地址
                String address = IpUtil.getGatwayIpAddress(request);
                if(!address.equals(ipAddress)){
                    //ip不一致，令牌被盗用。重新登录
                    return redirectLogin(exchange);
                }

            }
        }



        //5、如果没带token；或者带对了token，就进行 【用户id或临时id】 透传
        //5.2、合法的token进行用户id透传
        return userIdThrought(exchange,chain,info);


        //        Mono<Void> mono = chain.filter(exchange)
//                .doFinally(item -> {
//                    log.info("{} 请求结束；", requestPath);  //感知结束的正确时机
//                });

        //6、如果没带也直接放行
//        return chain.filter(exchange);
    }


    /**
     * 用户id透传
     * @param exchange
     * @param chain
     * @param info
     * @return
     */
    private  Mono<Void> userIdThrought(ServerWebExchange exchange,
                                             GatewayFilterChain chain,
                                             UserInfo info) {
        //无论登录还是不登录，前端都会带一个 UserTempId ，把这个无条件透传；
        // UserTempId 和 token 一样，
        // 1、前端如果发送普通请求会带在cookie里【浏览器默认机制】，
        // 2、如果发送ajax请求会带在请求头里【前端代码机制】
        // 我们效果：无论是在cookie里还是在头里，最终微服务只需要从header中获取 UserTempId 或 UserId


        // 登录通过，有用户id，就把用户id往下透传；【请求头中加上UserId字段】
        ServerHttpRequest.Builder requestBuilder = exchange.getRequest()
                .mutate();

        if(info != null){
            //登录了透传UserId
            requestBuilder.header(RedisConst.USER_ID_HEADER,info.getId().toString());
        }
        // UserTempId 总是有，也直接放到头里面

        String UserTempId = getCookieOrHeaderValue(exchange, RedisConst.USER_TEMPID_HEADER);
        if(StringUtils.hasText(UserTempId)){
            requestBuilder.header(RedisConst.USER_TEMPID_HEADER,UserTempId);
        }


        ServerHttpRequest request = requestBuilder.build();

        ServerWebExchange newExchange = exchange.mutate()
                .request(request) //用新请求
                .response(exchange.getResponse()) //响应没改不用管
                .build();
        log.info("【{}】请求必须登录，且校验通过，id已经透传，放行...", request.getPath().value());
        //放行
        return chain.filter(newExchange);
    }


    /**
     * 重定向到登录页
     * @param exchange
     * @return
     */
    private Mono<Void> redirectLogin(ServerWebExchange exchange) {
        ServerHttpResponse response = exchange.getResponse();
        ServerHttpRequest request = exchange.getRequest();
        String url = "";
        try {
            //请求的完整url地址
            url = request.getURI().toURL().toString();
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
        //设置302状态码
        response.setStatusCode(HttpStatus.FOUND);
        //设置Location响应头
        String loginUrl = urlProperties.getLoginUrl();
        loginUrl += "?originUrl="+url;
        response.getHeaders().set("Location",loginUrl);


        //重定向之前，删除假cookie
        ResponseCookie cookie = ResponseCookie
                .from("token", "1111")
                .maxAge(0)
                .domain(".gmall.com")
                .build();
        response.addCookie(cookie);

        //移除cookie

//                response.setStatusCode() //设置响应状态码
//                response.getHeaders().set(); //设置响应头
//                response.writeWith(); //写响应体数据
        return response.setComplete();
    }

    private UserInfo getLoginUserInfo(String token) {
        String json = redisTemplate.opsForValue().get(RedisConst.LOGIN_USER + token);

        return JSONs.jsonStr2Obj(json, UserInfo.class);
    }


    /**
     * token的值，不一定在header中，也有可能在cookie中，自己要能拿出来
     *
     * @param exchange
     * @return
     */
    private String getCookieOrHeaderValue(ServerWebExchange exchange,String key) {
        ServerHttpRequest request = exchange.getRequest();

        //1、获取token头的值；  先检查token头【针对ajax请求】
        List<String> token = request.getHeaders().get(key);
        if (token == null || token.size() == 0) {
            //2、再检查cookie；
            HttpCookie cookie = request.getCookies().getFirst(key);
            if (cookie != null) {
                return cookie.getValue();
            }
            //3、cookie和header都没有token
            return null;
        }

        //4、说明header有值
        return token.get(0);
    }

    private static Mono<Void> responseJSON(ServerWebExchange exchange, Object json) {
        ServerHttpResponse response = exchange.getResponse();
        //构造响应数据
        DataBuffer buffer = response
                .bufferFactory()
                .wrap(JSONs.obj2JsonStr(json).getBytes(StandardCharsets.UTF_8));
        //防乱码响应头  Content-Type: application/json;charset=UTF-8
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
//        response.getHeaders().add("Content-Type","application/json;charset=UTF-8");
        Mono<Void> mono = response.writeWith(Mono.just(buffer));
        return mono;
    }

}
